use rhai::plugin::*;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::RwLock;

pub struct EnergizerBunny {
    speed: i64,
    is_going: bool,
}

impl EnergizerBunny {
    pub fn new() -> Self {
        EnergizerBunny {
            speed: 0,
            is_going: false,
        }
    }
    pub fn go(&mut self) {
        println!("Go!");
        self.is_going = true;
    }
    pub fn stop(&mut self) {
        println!("Stop!");
        self.is_going = false;
    }
    pub fn is_going(&self) -> bool {
        self.is_going
    }
    pub fn get_speed(&self) -> i64 {
        self.speed
    }
    pub fn set_speed(&mut self, speed: i64) {
        self.speed = speed
    }
    pub fn turn(&mut self, left_turn: bool) {
        println!("turn left?{}", left_turn)
    }
}

pub type SharedBunny1 = Rc<RefCell<EnergizerBunny>>;
pub type SharedBunny2 = Arc<RwLock<EnergizerBunny>>;
pub type SharedBunny3 = Arc<Mutex<EnergizerBunny>>;

// use rhai::plugin::*;

// Remember to put 'pure' on all functions, or they'll choke on constants!
#[export_module]
pub mod bunny_api1 {
    use rhai::export_module;

    // Custom type 'SharedBunny' will be called 'EnergizerBunny' in scripts
    pub type EnergizerBunny = SharedBunny1;

    // This constant is also available to scripts
    pub const MAX_SPEED: i64 = 100;

    #[rhai_fn(get = "power", pure)]
    pub fn get_power(bunny: &mut EnergizerBunny) -> bool {
        bunny.borrow().is_going()
    }
    #[rhai_fn(set = "power", pure)]
    pub fn set_power(bunny: &mut EnergizerBunny, on: bool) {
        if on {
            if bunny.borrow().is_going() {
                println!("Still going...");
            } else {
                bunny.borrow_mut().go();
            }
        } else {
            if bunny.borrow().is_going() {
                bunny.borrow_mut().stop();
            } else {
                println!("Already out of battery!");
            }
        }
    }
    #[rhai_fn(get = "speed", pure)]
    pub fn get_speed(bunny: &mut EnergizerBunny) -> i64 {
        if bunny.borrow().is_going() {
            bunny.borrow().get_speed()
        } else {
            0
        }
    }
    #[rhai_fn(set = "speed", pure, return_raw)]
    pub fn set_speed(bunny: &mut EnergizerBunny, speed: i64) -> Result<(), Box<EvalAltResult>> {
        if speed <= 0 {
            Err("Speed must be positive!".into())
        } else if speed > MAX_SPEED {
            Err("Bunny will be going too fast!".into())
        } else if !bunny.borrow().is_going() {
            Err("Bunny is not yet going!".into())
        } else {
            bunny.borrow_mut().set_speed(speed);
            Ok(())
        }
    }
    #[rhai_fn(pure)]
    pub fn turn_left(bunny: &mut EnergizerBunny) {
        if bunny.borrow().is_going() {
            bunny.borrow_mut().turn(true);
        }
    }
    #[rhai_fn(pure)]
    pub fn turn_right(bunny: &mut EnergizerBunny) {
        if bunny.borrow().is_going() {
            bunny.borrow_mut().turn(false);
        }
    }
}

#[export_module]
pub mod bunny_api2 {
    // Custom type 'SharedBunny' will be called 'EnergizerBunny' in scripts
    pub type EnergizerBunny = SharedBunny2;

    // This constant is also available to scripts
    pub const MAX_SPEED: i64 = 100;

    #[rhai_fn(get = "power", pure)]
    pub fn get_power(bunny: &mut EnergizerBunny) -> bool {
        bunny.read().unwrap().is_going()
    }
    #[rhai_fn(set = "power", pure)]
    pub fn set_power(bunny: &mut EnergizerBunny, on: bool) {
        if on {
            if bunny.read().unwrap().is_going() {
                println!("Still going...");
            } else {
                bunny.write().unwrap().go();
            }
        } else {
            if bunny.read().unwrap().is_going() {
                bunny.write().unwrap().stop();
            } else {
                println!("Already out of battery!");
            }
        }
    }
    #[rhai_fn(get = "speed", pure)]
    pub fn get_speed(bunny: &mut EnergizerBunny) -> i64 {
        if bunny.read().unwrap().is_going() {
            bunny.read().unwrap().get_speed()
        } else {
            0
        }
    }
    #[rhai_fn(set = "speed", pure, return_raw)]
    pub fn set_speed(bunny: &mut EnergizerBunny, speed: i64) -> Result<(), Box<EvalAltResult>> {
        if speed <= 0 {
            Err("Speed must be positive!".into())
        } else if speed > MAX_SPEED {
            Err("Bunny will be going too fast!".into())
        } else if !bunny.read().unwrap().is_going() {
            Err("Bunny is not yet going!".into())
        } else {
            bunny.write().unwrap().set_speed(speed);
            Ok(())
        }
    }
    #[rhai_fn(pure)]
    pub fn turn_left(bunny: &mut EnergizerBunny) {
        if bunny.read().unwrap().is_going() {
            bunny.write().unwrap().turn(true);
        }
    }
    #[rhai_fn(pure)]
    pub fn turn_right(bunny: &mut EnergizerBunny) {
        if bunny.read().unwrap().is_going() {
            bunny.write().unwrap().turn(false);
        }
    }
}

#[export_module]
pub mod bunny_api3 {
    // Custom type 'SharedBunny' will be called 'EnergizerBunny' in scripts
    pub type EnergizerBunny = SharedBunny3;

    // This constant is also available to scripts
    pub const MAX_SPEED: i64 = 100;

    #[rhai_fn(get = "power", pure)]
    pub fn get_power(bunny: &mut EnergizerBunny) -> bool {
        bunny.lock().unwrap().is_going()
    }
    #[rhai_fn(set = "power", pure)]
    pub fn set_power(bunny: &mut EnergizerBunny, on: bool) {
        if on {
            if bunny.lock().unwrap().is_going() {
                println!("Still going...");
            } else {
                bunny.lock().unwrap().go();
            }
        } else {
            if bunny.lock().unwrap().is_going() {
                bunny.lock().unwrap().stop();
            } else {
                println!("Already out of battery!");
            }
        }
    }
    #[rhai_fn(get = "speed", pure)]
    pub fn get_speed(bunny: &mut EnergizerBunny) -> i64 {
        if bunny.lock().unwrap().is_going() {
            bunny.lock().unwrap().get_speed()
        } else {
            0
        }
    }
    #[rhai_fn(set = "speed", pure, return_raw)]
    pub fn set_speed(bunny: &mut EnergizerBunny, speed: i64) -> Result<(), Box<EvalAltResult>> {
        if speed <= 0 {
            Err("Speed must be positive!".into())
        } else if speed > MAX_SPEED {
            Err("Bunny will be going too fast!".into())
        } else if !bunny.lock().unwrap().is_going() {
            Err("Bunny is not yet going!".into())
        } else {
            bunny.lock().unwrap().set_speed(speed);
            Ok(())
        }
    }

    #[rhai_fn(pure)]
    pub fn turn_left(bunny: &mut EnergizerBunny) {
        if bunny.lock().unwrap().is_going() {
            bunny.lock().unwrap().turn(true);
        }
    }

    #[rhai_fn(pure)]
    pub fn turn_right(bunny: &mut EnergizerBunny) {
        if bunny.lock().unwrap().is_going() {
            bunny.lock().unwrap().turn(false);
        }
    }
}

#[cfg(test)]
mod tests {
    use rhai::exported_module;
    use rhai::Engine;
    use rhai::Scope;

    use super::bunny_api2;
    use super::bunny_api3;
    use super::EnergizerBunny;
    use super::SharedBunny2;
    use super::SharedBunny3;
    use std::sync::Arc;
    use std::sync::Mutex;
    use std::sync::RwLock;

    const SCRIPT: &str = r#"
        // Access the command object via constant variable 'BUNNY'.

        if !BUNNY.power { BUNNY.power = true; }

        if BUNNY.speed > 50 { BUNNY.speed = 50; }

        BUNNY.turn_left();

        let BUNNY = 42;     // <- syntax error if variable definition filter is set
    "#;

    #[test]
    fn test_bunny2() {
        let mut engine = Engine::new();

        // ---------------
        // 2
        engine.register_global_module(exported_module!(bunny_api2).into());
        let bunny: SharedBunny2 = Arc::new(RwLock::new(EnergizerBunny::new()));
        // ---------------

        let ast = engine.compile(SCRIPT).unwrap();

        let mut scope = Scope::new();

        // Add the singleton command object into a custom Scope.
        // Constants, as a convention, are named with all-capital letters.
        scope.push_constant("BUNNY", bunny.clone());

        // Run the compiled AST
        engine.run_ast_with_scope(&mut scope, &ast).unwrap();

        // Running the script directly, as below, is less desirable because
        // the constant 'BUNNY' will be propagated and copied into each usage
        // during the script optimization step
        // engine.run_with_scope(&mut scope, script)?;

        //
    }

    #[test]
    fn test_bunny3() {
        let mut engine = Engine::new();

        // ---------------
        // 3
        engine.register_global_module(exported_module!(bunny_api3).into());
        let bunny: SharedBunny3 = Arc::new(Mutex::new(EnergizerBunny::new()));

        let ast = engine.compile(SCRIPT).unwrap();

        let mut scope = Scope::new();

        // Add the singleton command object into a custom Scope.
        // Constants, as a convention, are named with all-capital letters.
        scope.push_constant("BUNNY", bunny.clone());

        // Run the compiled AST
        engine.run_ast_with_scope(&mut scope, &ast).unwrap();

        // Running the script directly, as below, is less desirable because
        // the constant 'BUNNY' will be propagated and copied into each usage
        // during the script optimization step
        // engine.run_with_scope(&mut scope, script)?;

        //
    }
}
